package com.firemoss.atonement.mapper
{
	import com.firemoss.atonement.mapper.error.InvalidMappingsError;
	import com.firemoss.atonement.mapper.error.UnresolvedNameMapping;
	import com.firemoss.atonement.mapper.error.UnresolvedTypeMapping;
	import com.firemoss.atonement.mapper.resolution.IValueResolverFactory;
	import com.firemoss.atonement.mapper.resolution.MappedValueResolver;
	import com.firemoss.atonement.metadata.DefaultMetadataBuilder;
	import com.firemoss.atonement.metadata.IMetadataBuilder;
	import com.firemoss.atonement.metadata.Property;
	import com.firemoss.atonement.metadata.PropertyMetadata;
	
	import flash.utils.Dictionary;
	
	import mx.collections.IList;
	
	import org.as3commons.reflect.Type;

	public class Mapping
	{
		private var queuedPropertyResolvers : Array = new Array()
		private var queuedAggregates : Array = new Array()
		
		private var resolvedProperties : Dictionary = new Dictionary()
		private var unresolvedPropertiesByName : Dictionary = new Dictionary()
		private var unresolvedPropertiesByType : Dictionary = new Dictionary()
		private var aggregatedTargetProperties : Dictionary = new Dictionary()
			
		internal var metadataBuilder : IMetadataBuilder
		internal var validated : Boolean = false
			
		public var mapper : Mapper
		public var source : Class
		public var target : Class
		public var ignoredProperties : Array = []
		// Object, not dict, to allow MXML expression
		public var aggregatedPropertyTargetTypes : Object = {}
		public var honorAnnotations : Boolean = true
			
		public function get resolvedPropertyMap() : Dictionary
		{
			return resolvedProperties
		}
		
		public function set propertyResolvers( resolvers : Array ) : void
		{
			for each ( var resolver : PropertyResolver in resolvers )
			{
				// TODO:  refactor this horseshit.  addPropertyResolver() should just take a resolver instance.
				addPropertyResolver( resolver )
			}
		}
		
		public function Mapping( source : Class = null, target : Class = null, honorAnnotations : Boolean = true )
		{
			this.source = source
			this.target = target
			this.honorAnnotations = honorAnnotations
		}
		
		private function dequeueDelayedConfiguration() : void
		{
			addQueuedPropertyResolvers()
			addQueuedAggregates()
		}
		
		private function addQueuedPropertyResolvers() : void
		{
			while ( queuedPropertyResolvers.length > 0 )
			{
				var resolver : PropertyResolver = queuedPropertyResolvers.pop()
				addPropertyResolver.apply( this, [ resolver ] )
			}
		}
		
		private function addQueuedAggregates() : void
		{
			while ( queuedAggregates.length > 0 )
			{
				var args : Array = queuedAggregates.pop()
				setAggregatedType.apply( this, args )
			}
		}
		
		/**
		 * Adds mapping-based resolutions that have been registered with the assigned
		 * mapper _after_ this one.
		 */
		private function createLateMappedPropertyResolutions() : void
		{
			if ( !mapper )
			{
				throw new Error( "Can't resolve late mapped properties without a mapper!" );
			}
			
			for each ( var unresolvedProperty : UnresolvedTypeMapping in unresolvedPropertiesByType )
			{
				if ( mapper.mappingExistsBetween( unresolvedProperty.sourceProperty.type.clazz, unresolvedProperty.targetProperty.type.clazz ) )
				{
					var resolver : PropertyResolver = new PropertyResolver()
					resolver.sourceProperty = unresolvedProperty.sourceProperty.name 	
					resolver.targetProperty = unresolvedProperty.targetProperty.name 	
					resolver.valueResolutionFunction = MappedValueResolver.resolve 	
					
					addPropertyResolver( resolver )
				}
			}
		}
		
		internal function resolveProperties( resolverFactory : IValueResolverFactory ) : void
		{
			var sourceProperties : PropertyMetadata = metadataBuilder.buildMetadataForClass( source ).propertyMetadata	
			var targetProperties : PropertyMetadata = metadataBuilder.buildMetadataForClass( target ).propertyMetadata	
				
			for each ( var targetProperty : Property in targetProperties.properties )
			{
				// Don't overwrite explicitly resolved properties!
				if ( resolvedProperties[ targetProperty.name ] == null || !PropertyResolution( resolvedProperties[ targetProperty.name ] ).explicit )
				{
					
					if ( sourceProperties.containsProperty( targetProperty.name, !mapper.caseSensitivePropertyNames ) )
					{
						var sourceProperty : Property = sourceProperties.getProperty( targetProperty.name, !mapper.caseSensitivePropertyNames )
						
							
						// TODO:  Cook up a real way to consider not-alike types as being alike...not just this array/ilist crap.
						if ( sourceProperty.type == targetProperty.type  || resolverFactory.canResolveDifferentTypes( sourceProperty.type, targetProperty.type ) )
						{
							var resolution : PropertyResolution = new PropertyResolution()
								
							/*
							resolver.sourceProperty = sourceProperties.getProperty( targetProperty.name )
							resolver.targetProperty = targetProperties.getProperty( targetProperty.name )
						*/
							resolution.sourceProperty = sourceProperty
							resolution.targetProperty = targetProperty
							resolution.valueResolutionFunction = resolverFactory.create( sourceProperty.type, targetProperty.type ).resolve
								
							resolvedProperties[ targetProperty.name ] = resolution
						}
						else
						{
							var unresolvedByType : UnresolvedTypeMapping = new UnresolvedTypeMapping()
							unresolvedByType.source = source
							unresolvedByType.target = target
							unresolvedByType.sourceProperty = sourceProperty
							unresolvedByType.targetProperty = targetProperty
							unresolvedPropertiesByType[ targetProperty.name ] = unresolvedByType
						}
						
						// Is the target type an IList or Dictionary?  State that we need resolution of what to create when walking
						// the graph.
						try
						{
							var instance : * = new targetProperty.type.clazz()
							
							if ( instance is IList || instance is Dictionary )
							{
								aggregatedTargetProperties[ targetProperty.name ] = targetProperty
							}
						}
						catch ( e : Error )
						{
							// Well, that obviously wasn't something representing a collection, was it?
						}
					}
					else
					{
						var unresolvedByName : UnresolvedNameMapping = new UnresolvedNameMapping()
						unresolvedByName.source = source
						unresolvedByName.target = target
						unresolvedByName.targetProperty = targetProperty
						unresolvedPropertiesByName[ targetProperty.name ] = unresolvedByName
					}
					
					// Post-processed metadata
					if ( honorAnnotations )
					{
						if ( targetProperty.ignored )
						{
							ignoreProperty( targetProperty.name )
						}
						
						if ( targetProperty.collectionOf )
						{
							setAggregatedType( targetProperty.name, targetProperty.collectionOf )
						}
						
						if ( targetProperty.valueResolutionFunction != null )
						{
							var annotatedResolver : PropertyResolver = new PropertyResolver()
							annotatedResolver.targetProperty = targetProperty.name
							annotatedResolver.sourceProperty = targetProperty.sourcePropertyName
							annotatedResolver.valueResolutionFunction = targetProperty.valueResolutionFunction
							this.addPropertyResolver( annotatedResolver )
						}
					}
				}
			}
			
			dequeueDelayedConfiguration()
		}
		
		/**
		 * Adds a property resolver from the given source property name to the target
		 * property name, stating that the passed valueResolutionFunction will be used
		 * to perform the value resolution.  To state that the entire source instance
		 * should be passed to the resolution function, pass null for the source property.
		 */
		public function addPropertyResolver( resolver : PropertyResolver ) : Mapping
		{
			if ( !metadataBuilder )
			{
				queuedPropertyResolvers.push( resolver )
			}
			else
			{
				var sourceProperties : PropertyMetadata = metadataBuilder.buildMetadataForClass( source ).propertyMetadata	
				var targetProperties : PropertyMetadata = metadataBuilder.buildMetadataForClass( target ).propertyMetadata	
				
				/* Don't throw :  maybe warn if we add logging.  This puts knowledge of source in the target, which we can't always rely on. 
				if ( resolver.sourceProperty && !sourceProperties.containsProperty( resolver.sourceProperty, !mapper.caseSensitivePropertyNames ) )
				{
					throw new Error( "Can't add a custom property resolver for the source property \"" + resolver.sourceProperty + "\" because no such property exists on the source class, " + Type.forClass( source ).fullName );
				}
				*/
				if ( resolver.sourceProperty && !sourceProperties.containsProperty( resolver.sourceProperty ) )
				{
					// This resolver isn't valid for this mapping.
					return this;
				}
				
				if ( !targetProperties.containsProperty( resolver.targetProperty, !mapper.caseSensitivePropertyNames ) )
				{
					throw new Error( "Can't add a custom property resolver for the target property \"" + resolver.targetProperty + "\" because no such property exists on the target class, " + Type.forClass( target ).fullName );
				}
				
				var resolution : PropertyResolution = new PropertyResolution()
				
				resolution.sourceProperty = resolver.sourceProperty != null ? sourceProperties.getProperty( resolver.sourceProperty, !mapper.caseSensitivePropertyNames ) : null
				resolution.targetProperty = targetProperties.getProperty( resolver.targetProperty, !mapper.caseSensitivePropertyNames )
				resolution.valueResolutionFunction = resolver.valueResolutionFunction
				resolution.explicit = true
				
				resolvedProperties[ resolver.targetProperty ] = resolution;
				
				if ( unresolvedPropertiesByName[ resolver.targetProperty ] != null )
				{
					delete unresolvedPropertiesByName[ resolver.targetProperty ]
				}
				
				if ( unresolvedPropertiesByType[ resolver.targetProperty ] != null )
				{
					delete unresolvedPropertiesByType[ resolver.targetProperty ]
				}
			}
			
			return this
		}
		
		public function ignoreProperty( propertyName : String ) : Mapping
		{
			ignoredProperties.push( propertyName )
				
			return this
		}
		
		public function setAggregatedType( propertyName : String, clazz : Class ) : Mapping
		{
			if ( metadataBuilder )
			{
				var targetProperties : PropertyMetadata = metadataBuilder.buildMetadataForClass( target ).propertyMetadata	
				
				if ( !targetProperties.containsProperty( propertyName, !mapper.caseSensitivePropertyNames ) )
				{
					throw new Error( "Can't set the aggregated type for the target property \"" + propertyName + "\" because no such property exists on the target class, " + Type.forClass( target ).fullName );
				}
				
				aggregatedPropertyTargetTypes[ propertyName ] = clazz
			}
			else
			{
				queuedAggregates.push( [ propertyName, clazz ] )
				
			}
			return this
		}
		
		/**
		 * States if a given property (of the target class!) has been assigned
		 * a value resolution function.
		 */
		public function hasResolvedProperty( propertyName : String ) : Boolean
		{
			return resolvedProperties[ propertyName ] != null;
		}
		
		/**
		 * States if a given property (of the target class!) has been assigned
		 * is missing a value resolution function.
		 */
		public function hasUnresolvedProperty( propertyName : String ) : Boolean
		{
			return unresolvedPropertiesByName[ propertyName ] != null || unresolvedPropertiesByType[ propertyName ] != null
		}
		
		/**
		 * Checks for unresolved propertyies by name and type, printing suggested resolutions
		 * to the console and throwing an error if unresolved properties are found.
		 */
		public function assertMappingIsValid() : Boolean
		{
			var name : Array = []
			var type : Array = []
			var err : InvalidMappingsError
			var message : String = "MAPPING PROBLEMS: " + Type.forClass(source).name + " -> " + Type.forClass(target).name + "\n"
			
			// If some properties type mapping relies on Mappings establish _after_ this one
			// was added to the mapper, we need to check their resolution.
			createLateMappedPropertyResolutions()
			
			for each ( var byName : UnresolvedNameMapping in unresolvedPropertiesByName )
			{
				if ( ignoredProperties.indexOf( byName.targetProperty.name ) == -1 )
				{
					name.push( byName )
						
					message += byName.toString() + "\n"
				}
			}
			
			for each ( var byType : UnresolvedTypeMapping in unresolvedPropertiesByType )
			{
				if ( ignoredProperties.indexOf( byType.targetProperty.name ) == -1 )
				{
					type.push( byType )
					
					message += byType.toString() + "\n"
				}
			}

			// Validate our aggregated target types
			var missingTypeAggregation : Boolean = false;
			for each ( var targetProperty : * in aggregatedTargetProperties )
			{
				if ( !aggregatedPropertyTargetTypes.hasOwnProperty( targetProperty.name )
					&& (
						!resolvedPropertyMap.hasOwnProperty( targetProperty.name )
						|| PropertyResolution( resolvedPropertyMap[ targetProperty.name ] ).explicit == false
					)
					&& ( ignoredProperties.indexOf( targetProperty.name ) == -1 )
				)
				{
					missingTypeAggregation = true
						
					message += "\n-- MISSING AGGREGATED TYPE: For the target type " 
						+ Type.forClass( target ).fullName + "'s \"" + targetProperty.name + "\"\n\t"
						+ " property, you haven't stated what class to use to fill the collection.\n\t"
						+ " This means that the mapper can't map this property because it wouldn't know what types to create!\n\t"
						+ " Please use [CollectionOf(\"my.package.and.Type\")] annotations on the target property or \n\t"
						+  "call .setAggregatedType( \"" + targetProperty.name + "\", TypeName ) or add a manual property resolver to handle this property."
				}
			}
			
			if ( name.length > 0 || type.length > 0 || missingTypeAggregation )
			{
				trace( message )
				var e : InvalidMappingsError =  new InvalidMappingsError( name, type, message )
				throw( e )
			}		
			
			validated = true
				
			return true
		}
	}
}